package com.google.gwt.user.client.rpc.impl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.InvocationException;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.gwthtml.servlet.GWTContext;
import com.gwthtml.servlet.RemoteHtmlServiceServlet;

public class ServiceProxyHandler implements ServiceDefTarget, InvocationHandler {
	
	private String serviceEntryPoint;
	private RemoteHtmlServiceServlet service;
	Class syncIf;
	Class asyncIf;
		
	public ServiceProxyHandler(Class syncIf, Class asyncIf) {
		service = null;
		this.syncIf = syncIf;
		this.asyncIf = asyncIf;
	}
	
	public String getServiceEntryPoint() {
		return serviceEntryPoint;
	}

	public void setServiceEntryPoint(String address) {
		serviceEntryPoint = address;
		service = null;
	}

	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		// First try to invoke the ServiceDefTarget methods on ourselves
		for (Method m : ServiceDefTarget.class.getDeclaredMethods()) {
			if (m.equals(method)) {
				return m.invoke(this, args);
			}
		}
		
		// OK, let's do the hard work
		if (service == null) {
			Map<String, RemoteHtmlServiceServlet> services = (Map<String, RemoteHtmlServiceServlet>) GWTContext
					.getServletContext().getAttribute(
							RemoteHtmlServiceServlet.REMOTE_SERVICE_KEY);
			if (services == null) {
				throw new InvocationException(
						"Unable to find Service Entry Point: "
								+ serviceEntryPoint);
			}
			String serviceLocalPath = serviceEntryPoint.replaceFirst(GWTContext.getServletRequest().getContextPath(), "");
			service = services.get(serviceLocalPath);
			if (service == null) {
				throw new InvocationException(
						"Unable to find Service Entry Point: "
								+ serviceEntryPoint);
			}
		}
		AsyncCallback cb = null;
		try {
			Method m = method; 
			Object[] argsToCall = args;
			if (args[args.length -1 ] instanceof AsyncCallback) {
				// Strip asyncCallBack from there
				argsToCall = copyOfRange(args, 0, args.length - 1);
				m = findMethodToCall(method);
				cb = (AsyncCallback) args[args.length - 1];
			}
			Object result = service.invoke(m, argsToCall, GWTContext.getServletRequest(), GWTContext.getServletResponse());
			if (cb == null) {
				return result;
			}
			cb.onSuccess(result);
		} catch (IllegalArgumentException t) {
			sendInternalError(cb, t);
		} catch (IllegalAccessException t) {
			sendInternalError(cb, t);
		} catch (InvocationTargetException t) {
			sendInternalError(cb, t);
		} catch (Throwable t) {
			sendError(cb, t);
		}
		return null;
	}

	private Method findMethodToCall(Method method) {
		Class[] paramTypes =  copyOfRange(method.getParameterTypes(), 0, method.getParameterTypes().length-1);
		for (Method m : syncIf.getDeclaredMethods()) {
			if (m.getName().equals(method.getName())
				&& Arrays.equals(paramTypes,m.getParameterTypes())) {
				return m;
			}
		}
		return method;
	}

	private void sendError(AsyncCallback cb, Throwable t) throws Throwable {
		if (cb == null)
			throw t;
		cb.onFailure(t);			
	}
	
	private void sendInternalError(AsyncCallback cb, Throwable t) throws Throwable {
		InvocationException ie = new InvocationException("Invocation failed with Exception", t); 
		sendError(cb, ie);
	}
	
	private Class[] copyOfRange(Class[] array, int startIndex, int endIndex) {
		int size = endIndex - startIndex;
		Class[] result = (Class[]) new Class[size];
		int j = 0;
		for (int i = startIndex; i < endIndex; i++) {
			result[j++] = array[i]; 
		}
		return result;
	}

	private Object[] copyOfRange(Object[] array, int startIndex, int endIndex) {
		int size = endIndex - startIndex;
		Object[] result = (Object[]) new Object[size];
		int j = 0;
		for (int i = startIndex; i < endIndex; i++) {
			result[j++] = array[i]; 
		}
		return result;
	}
}
